﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using RoketSolutions.SP2010.Infrostructure.UI.Ajax.Abstractions;
using RoketSolutions.SP2010.Infrostructure.UI.Ajax.ModelBinders.Interfaces;
using RoketSolutions.SP2010.Infrostructure.Common.Extensions;
using RoketSolutions.SP2010.Infrostructure.UI.Ajax.ValueProviders;

namespace RoketSolutions.SP2010.Infrostructure.UI.Ajax.ModelBinders
{
    public class DefaultModelBinder : IModelBinder
    {
        #region / Bind Methods /

        public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            if (bindingContext == null)
                throw new ArgumentNullException("bindingContext");

            var modelName = bindingContext.ModelName;
            if (modelName.IsNullOrEmpty())
                return null;

            // check whether an any value provider contains corresponding value for specified model
            if (bindingContext.ValueProvider.ContainsPrefix(modelName))
            {
                var providerValue = bindingContext.ValueProvider.GetValue(modelName);
                return BindSimpleModel(controllerContext, bindingContext, providerValue);
            }

            // check whether the specified model is an array
            if (bindingContext.ModelType.IsArray())
            {
                return BindArrayModel(controllerContext, bindingContext);
            }

            // check whether the specified model is complex type
            if (bindingContext.ModelType.IsComplexType())
            {
                return BindComplexModel(controllerContext, bindingContext);
            }

            return null;
        }

        protected object BindArrayModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            // get the type of the array's element
            var arrayElementType = bindingContext.ModelType.FindGenericInterface(typeof (IEnumerable<>)).GetGenericArguments().First();
            
            // check whether the array of simple types
            if (!arrayElementType.IsComplexType())
            {
                var modelPrefix = bindingContext.ModelName + "[]";
                if (bindingContext.ValueProvider.ContainsPrefix(modelPrefix))
                {
                    var providerValue = bindingContext.ValueProvider.GetValue(modelPrefix);
                    return BindSimpleModel(controllerContext, bindingContext, providerValue);
                }
                return null;
            }

            // we have the array of complex types

            var modelPrefixPattern = string.Format("{0}\\[\\d*\\]", bindingContext.ModelName.Replace("[", "\\[").Replace("]", "\\]"));
            if (bindingContext.ValueProvider.ContainsMatchPrefix(modelPrefixPattern))
            {
                var resultArray = (IList)CreateModel(controllerContext, bindingContext);

                IModelBinder binder;
                if (!controllerContext.Controller.ModelBinders.TryGetValue(arrayElementType, out binder))
                {
                    binder = controllerContext.Controller.ModelBinders.DefaultBinder;
                }
                var prefixes = bindingContext.ValueProvider.GetMatchPrefix(modelPrefixPattern);
                foreach (var arrayElementPrefix in prefixes)
                {
                    var elementContext = new ModelBindingContext
                                             {
                                                 ModelName = arrayElementPrefix,
                                                 ModelType = arrayElementType,
                                                 ValueProvider = bindingContext.ValueProvider
                                             };
                    var element = binder.BindModel(controllerContext, elementContext);
                    if (element != null)
                    {
                        resultArray.Add(element);
                    }
                }
                return resultArray;
            }
            return null;
        }

        protected object BindSimpleModel(ControllerContext controllerContext, ModelBindingContext bindingContext, ValueProviderResult providerValue)
        {
            if (bindingContext.ModelType != typeof(string))
            {
                // check whether the specified model is IEnumerable
                var modelEnumerableType = bindingContext.ModelType.FindGenericInterface(typeof(IEnumerable<>));
                if (modelEnumerableType != null)
                {
                    // create model object
                    var model = CreateModel(controllerContext, bindingContext);

                    // create corresponding array
                    var arrayElementType = modelEnumerableType.GetGenericArguments().First();
                    var arrayType = arrayElementType.MakeArrayType();

                    // fill array with provider values
                    var resultArray = providerValue.ConvertTo(arrayType);

                    // try fill model with array values
                    var sourceList = resultArray as IList;
                    var destinationList = model as IList;
                    if (sourceList != null && destinationList != null)
                    {
                        foreach (var sourceValue in sourceList)
                        {
                            destinationList.Add(sourceValue);
                        }
                    }
                    return model;
                }
            }

            // convert provider value to model type (possible types: string, int, double, datetime, etc)
            return providerValue.ConvertTo(bindingContext.ModelType);
        }

        protected object BindComplexModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            // create model object
            var model = CreateModel(controllerContext, bindingContext);

            // fetch each property and apply it to the model
            foreach (var property in GetBindableProperties(controllerContext, bindingContext))
            {
                var propertyValue = GetPropertyValue(controllerContext, bindingContext, property);
                property.SetValue(model, propertyValue);
            }

            return model;
        }

        #endregion

        #region / Helper Methods /

        protected object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            var modelType = bindingContext.ModelType;
            if (modelType.IsGenericType)
            {
                var genericTypeDefinition = modelType.GetGenericTypeDefinition();
                if (genericTypeDefinition == typeof(IDictionary<,>))
                {
                    modelType = typeof(Dictionary<,>).MakeGenericType(modelType.GetGenericArguments());
                }
                else if (((genericTypeDefinition == typeof(IEnumerable<>)) || (genericTypeDefinition == typeof(ICollection<>))) || (genericTypeDefinition == typeof(IList<>)))
                {
                    modelType = typeof(List<>).MakeGenericType(modelType.GetGenericArguments());
                }
            }
            return Activator.CreateInstance(modelType, true);
        }

        protected IEnumerable<PropertyDescriptor> GetBindableProperties(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            var propertyFilter = bindingContext.PropertyFilter;
            var propertyCollection = TypeDescriptor.GetProperties(bindingContext.ModelType);
            return
                propertyCollection.Cast<PropertyDescriptor>()
                    .Where(property => !property.IsReadOnly && (propertyFilter == null || propertyFilter(property.Name)))
                    .ToList();
        }

        protected object GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor)
        {
            var prefix = BuildComplexModelPrefix(bindingContext, propertyDescriptor);
            if (prefix.IsNullOrEmpty())
                return null;

            var propertyType = propertyDescriptor.PropertyType;
            IModelBinder binder;
            if (!controllerContext.Controller.ModelBinders.TryGetValue(propertyType, out binder))
            {
                binder = controllerContext.Controller.ModelBinders.DefaultBinder;
            }

            var propertyBindingContext = new ModelBindingContext
                                             {
                                                 ModelName = prefix,
                                                 ModelType = propertyType,
                                                 ValueProvider = bindingContext.ValueProvider
                                             };

            return binder.BindModel(controllerContext, propertyBindingContext);
        }

        protected string BuildComplexModelPrefix(ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor)
        {
            var modelName = bindingContext.ModelName;
            var propName = propertyDescriptor.Name;

            var appendModelName = modelName.Contains("[") || modelName.Contains(".");

            var jsonFullPrefx = string.Format("{0}[{1}]", modelName, propName);
            var formFullPrefix = string.Format("{0}.{1}", modelName, propName);
            var briefPrefix = appendModelName
                                      ? modelName.Contains("[")
                                            ? jsonFullPrefx
                                            : formFullPrefix
                                      : propName;

            var prefix = string.Empty;

            if (bindingContext.ValueProvider.ContainsPrefix(jsonFullPrefx))
            {
                prefix = jsonFullPrefx;
            }
            else if (bindingContext.ValueProvider.ContainsPrefix(formFullPrefix))
            {
                prefix = formFullPrefix;
            }
            else if (bindingContext.ValueProvider.ContainsPrefix(briefPrefix))
            {
                prefix = briefPrefix;
            }

            if (prefix.IsNullOrEmpty() && propertyDescriptor.PropertyType.IsComplexType())
            {
                if (bindingContext.ValueProvider.ContainsPrefix(jsonFullPrefx, false))
                    prefix = jsonFullPrefx;
                else if (bindingContext.ValueProvider.ContainsPrefix(formFullPrefix, false))
                    prefix = formFullPrefix;
                else if (bindingContext.ValueProvider.ContainsPrefix(briefPrefix, false))
                    prefix = briefPrefix;
            }

            return prefix;
        }

        #endregion

    }
}